Skip to content

feat(assert): add assert_command_available - #1033

Merged
Chemaclass merged 4 commits into
TypedDevs:mainfrom
fzlzjerry:feat/assert-command-available
Aug 9, 2026
Merged

feat(assert): add assert_command_available#1033
Chemaclass merged 4 commits into
TypedDevs:mainfrom
fzlzjerry:feat/assert-command-available

Conversation

@fzlzjerry

@fzlzjerry fzlzjerry commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Background

Related #1027

bashunit already exposes bashunit::is_command_available, but users could not assert that a required command resolves with a diagnostic that names the missing command. The workaround — assert_true "bashunit::is_command_available jq" — reported the failure as a boolean over an opaque string.

Changes

  • add assert_command_available <command> on top of the existing availability helper, so there is one definition of "available". A missing argument is a usage error (feat(assert): a missing argument reports a failed assertion, not a usage error #983 precedent); an optional second argument overrides the failure label, matching the rest of src/assert/core.sh
  • cover external commands, shell builtins, shell functions, the missing-command message, the custom label, the assertion counters, and the inverse relationship with assert_command_not_found in both directions
  • generalise tests/unit/assert/arity_test.sh: it now builds the "one argument short" call from the declared arity instead of branching on it, so the 1-argument case needed no new branch
  • add the assertion to the documentation, the generated bashunit doc snapshot, both completion scripts, the changelog, the agent skill catalogue and the assertion counts

Maintainer notes

Rebased onto main (conflict was the ## Unreleased changelog block against #1007, #1008 and #1035) and refactored on top of the contributor's commits:

  • trimmed the doc block to the house Arguments: form — the note about calling bashunit::assert::fail_with instead of the bashunit::assertion_failed facade repeated fail_with's own documented contract
  • dropped the added tests/acceptance/bashunit_hook_failure_test.sh case. It spent a full CLI run pinning the facade's frame depth for an assertion that never routes through the facade, and the two existing guards in that file already pin fail_with's depth generically
  • added the symmetry test the issue asks for (assert_command_available and assert_command_not_found never agree about the same command)

Validation

  • ./bashunit tests/ — 1786 passed, 13 skipped, 4 incomplete, 2 snapshot
  • ./bashunit --parallel tests/ — 1747 passed, 11 skipped, 4 incomplete, 2 snapshot
  • /bin/bash ./bashunit --strict tests/unit/assert/arity_test.sh on Bash 3.2 — exercises the empty-array set -u guard in the new arity loop
  • make sa and make lint — OK
  • ./build.sh + tests/acceptance/bashunit_test.sh — built binary docs match dev docs
  • mutation check: relaxing the new arity guard to -lt 0 turns the arity test red, so it is not vacuous

Checklist

  • I updated the CHANGELOG.md to reflect the new feature or fix
  • I updated the documentation to reflect the changes

@fzlzjerry
fzlzjerry force-pushed the feat/assert-command-available branch from 1b18889 to 126b369 Compare August 9, 2026 18:11
@Chemaclass

Copy link
Copy Markdown
Member

Reviewed and pushed two follow-up commits to this branch.

Blocker fixed (f03f8db). assert_command_available reported through bashunit::assertion_failed, the public facade meant for user-defined custom assertions. That facade adds a stack frame on top of bashunit::assert::fail_with, whose label fallback (fallback_depth 3) compensates for exactly one. With no test_* frame on the stack the label resolved to the facade:

✗ Failed: Bashunit::assertion failed        # before, failing inside set_up
✗ Failed: Assert command available          # after

Same class of regression already pinned for bashunit::assert::label in tests/acceptance/bashunit_hook_failure_test.sh; added a case there for the facade frame too. Built-in assertions must call bashunit::assert::fail_with / bashunit::state::add_assertions_passed directly, like every other assertion in src/assert/core.sh.

Also in that commit:

  • dropped the trailing space in "to be available but was "print_failed_test already emits one, so the output read to be available but was 'not found'
  • accepted the conventional label override as $2 (matches assert_empty / assert_not_empty), with a test
  • replaced test_assert_command_available_is_symmetric_with_assert_command_not_found: it executed the command and re-asserted that a missing one exits 127, which is assert_command_not_found's contract, not a symmetry between the two assertions (they aren't inverses — one checks resolution, the other an exit code). Swapped for a counter-movement test
  • reworded the docs link so bashunit doc renders it like the other cross-references, and regenerated the snapshot

Docs (01f3442). Added the name to the "Exit codes" bullet in docs/public/bashunit-skill.md — the count was bumped there but the list is what keeps an agent from inventing assertion names.

The 73 → 75 bump is correct, by the way, not an off-by-one: main was stale by two. 75 non-spy assertion functions; the 7 spy assertions are the "plus spies".

Local gate green: full suite, --parallel, make sa, make lint.

@Chemaclass
Chemaclass marked this pull request as ready for review August 9, 2026 20:05
@Chemaclass
Chemaclass enabled auto-merge (squash) August 9, 2026 20:14
@Chemaclass
Chemaclass force-pushed the feat/assert-command-available branch from 01f3442 to 9d89d1c Compare August 9, 2026 20:17
Copilot AI lite review requested due to automatic review settings August 9, 2026 20:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

fzlzjerry and others added 4 commits August 9, 2026 22:18
bashunit::assertion_failed is the facade for user-defined custom
assertions; it adds a stack frame on top of bashunit::assert::fail_with,
whose label fallback compensates for exactly one. An assertion failing
with no test_* frame on the stack (e.g. inside set_up) was therefore
labelled "Bashunit::assertion failed" instead of "Assert command
available" -- the same class of regression already pinned for
bashunit::assert::label in bashunit_hook_failure_test.sh.

Call bashunit::assert::fail_with / bashunit::state::add_assertions_passed
directly like every other assertion in the file, and cover the facade
frame in bashunit_hook_failure_test.sh.

Also:
- drop the trailing space in the failure condition, which doubled the
  space before the actual value
- accept the conventional label override as $2
- replace the assert_command_not_found "symmetry" test, which only
  restated that a missing command exits 127, with a counter-movement
  test and a custom-label test
- reword the docs link so `bashunit doc` renders it like the others

Claude-Session: https://claude.ai/code/session_01VdeSjVXBGPWELdq5iDdczY
The skill file already bumped the assertion count but left the new name
out of the "Exit codes" bullet, and that list is what an agent reads to
avoid inventing assertion names.

Claude-Session: https://claude.ai/code/session_01VdeSjVXBGPWELdq5iDdczY
…rage

- trim the doc block to the house Arguments form; the note about calling
  fail_with instead of the assertion_failed facade duplicated fail_with's
  own documented contract
- name the local `command`, matching the documented signature and the
  doubles modules
- drop the hook-failure acceptance test: it spends a full CLI run pinning
  the facade frame depth for an assertion that never routes through the
  facade, and the two existing guards already pin fail_with's depth
  generically
- build the arity probe's argument list from the required count instead of
  branching on it, so a new arity needs no new branch
- assert the inverse relationship with assert_command_not_found in both
  directions (issue acceptance criterion)
@Chemaclass
Chemaclass force-pushed the feat/assert-command-available branch from 9d89d1c to 8b3c115 Compare August 9, 2026 20:23
@Chemaclass Chemaclass added the enhancement New feature or request label Aug 9, 2026

@Chemaclass Chemaclass left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed after rebasing onto main and refactoring on top of the original commits.

Blockers: none.

Checked

  • Bash 3.0+: no declare -A, [[ ]], ${var,,}, negative indexing or &>>. The one new array (args in arity_test.sh) is appended with arr[${#arr[@]}]= and expanded as "${args[@]+"${args[@]}"}", so the empty case does not trip set -u on Bash 3.x — verified with /bin/bash ./bashunit --strict on 3.2.57.
  • Availability has one definition: the assertion delegates to bashunit::is_command_available, so builtins and shell functions behave identically to the helper, and the command is resolved rather than executed.
  • Failure output follows the fail_with voice of the neighbouring file/folder assertions and names the command. Custom label goes in $2, matching the rest of src/assert/core.sh.
  • Missing argument is a usage error (exit 2), per the #983 precedent, and the arity provider covers it. Mutation check: relaxing the guard to -lt 0 turns the arity test red, so the case is real.
  • Docs, bashunit doc snapshot, both completion scripts, the agent skill catalogue, the assertion counts and the changelog are all updated. ./build.sh plus tests/acceptance/bashunit_test.sh confirm the built binary's docs match dev docs.
  • make sa and make lint are clean; full and --parallel suites pass locally.

Nit, not acted on: Expected 'jq' / to be available but 'not found' reads a little stiff, but it is the same shape as to exist but 'do not exist' in src/assert/files.sh, so it stays consistent with the family.

Approving; merging once every CI job is green.

@Chemaclass
Chemaclass merged commit e379438 into TypedDevs:main Aug 9, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants